Flutter 基于Dio的网络框架

flutter作为跨平台提效方案,确实可以将开发的效率大幅提升,前提是:

  1. 尽量少的桥接
  2. 尽量多的复用

从这两个出发点延伸,接入Flutter后,会承接更多的业务逻辑,增加代码的平台性。基于这个原则,flutter将会承接工程中的主力。网络库的构建,也成了不可缺少的工作。

这篇文章中不在介绍网络选型的逻辑,所有的代码都是基于DIO网络通道构建的。

网络框架分析

客户端在构建的时候,往往都会在网络层的使用上封装一层,常见的封装有:

  1. 基于函数的封装
  2. 面向协议封装

一、前者常见的调用方式为:

[***Network get:参数 success:成功回调 failure:失败回调];

一般都是类函数的方式调用,封装的主要体现在:

  1. 简单API的输出
  2. 针对业务功能的Header聚合和封装
  3. baseURL的收敛
  4. post get 通过api区分
  5. 共有参数的收集
  6. 失败和成功的统一处理
  7. 入口出口的统一处理

在调用的时候业务只需要准备好对应的参数即可。但是这种方案的设计也存在一些缺陷

  1. header的修改
  2. baseUrl的修改
  3. 请求的逻辑不够聚合,零散分布问题 (都是函数的调用,散落在不同的业务文件中)

所以这种方式未必是你需要的网络封装

二、后者常见的调用方式为

[[[***Instance alloc] initWithParam:参数] requestWithSuccess:成功回调 failure:失败回调];

这里存在一个显著的区别,最终调用的对象是一个实例,而不是一个函数,这里封装主要体现在

  1. 功能点的分离
  2. 逻辑的拆解
  3. 业务穿插的节点
  4. 网络传输通道的唯一性

功能落地

下面就来看看在Flutter中如何实现这一套方案,首先功能点的分离,对于网络对象而言,他是有自己的内部的结构的,我们将这样的内部结构抽象到一个虚类中

/*
 * 网络接口协议定义
 */
abstract class HttpApi {
  String baseURL;
  String path;
  String method;
  Map<String, dynamic> parameters;
  Map<String, String> headers;
  ResultData result;

  String get url => baseURL + path;
}

在使用的时候为了便于操作,url是直接通过baseURL + path的方式得到的。业务在使用的时候,会适配一些通用的配置,例、

/*
 *业务适配器,在这里可以配置对应的baseURl 和 通用的header
 */
class BusinessApi extends HttpApi {

  @override
  // TODO: implement baseURL
  String get baseURL => "https://yourDomain.com";

  @override
  // TODO: implement headers
  Map<String, String> get headers => HttpHeader.defaultHeader;

}

这样,网络请求在不复写的情况下,都可以使用这两个默认值了。
为了让所有的网络请求都走到统一的网络通道中(方便之后的监控,入口和出口统一,可以做很多统计事件),需要将网络通道独立处理

/*
 * 网络通道
 */
class NetworkPipe extends BusinessApi {

  Future<ResultData> request<T> () async {
    // 准备数据源
    Map<String, String> headers = new HashMap();

    // 默认的header需要加上
    if (HttpHeader.defaultHeader != null) {
      headers.addAll(HttpHeader.defaultHeader);
    }

    // 当前header
    if (this.headers != null) {
      headers.addAll(this.headers);
    }

    Options option = new Options(method: this.method);
    ///超时
    option.connectTimeout = 15000;
    ///header
//    option.headers = headers;

    ///网络请求对象
    Dio dio = new Dio();

    Response response;
    // 判断当前的请求类型
    try {
      print("param");
      print(this.parameters);
      response = await dio.request(url, data: this.parameters, options: option);
    } on DioError catch (e) {
      // 请求错误处理
      Response errorResponse;
      if (e.response != null) {
        errorResponse = e.response;
      } else {
        errorResponse = new Response(statusCode: 666);
      }
      if (e.type == DioErrorType.CONNECT_TIMEOUT) {
        errorResponse.statusCode = ErrorCode.NETWORK_TIMEOUT;
      }
      print('请求异常: ' + e.toString());
      print('请求异常 url: ' + url);
      return new ResultData(errorResponse.statusCode, e.message, response.data);
    }

    // 对得到的结果进行解析
    try {
      if (option.contentType != null && option.contentType.primaryType == "text") {
        print("1");
        return new ResultData(response.statusCode, ErrorCode.errorDescriptionWithCode(response.statusCode), response.data);
      } else {
        print("2");
        var responseJson = response.data;
        if (response.statusCode == 201 && responseJson["token"] != null) {
          // 这里需要认证
        }
      }
      if (response.statusCode == 200 || response.statusCode == 201) {
        print("3");
        T result = EntityFactory.generateOBJ<T>(response.data);
        if (result != null) {
          print("4");
          print(result);
          print(T.toString());
          this.result = ResultData<T>(ErrorCode.SUCCESS, ErrorCodeDescription.NETWORK_SUCCESS, result);
          return new ResultData<T>(ErrorCode.SUCCESS, ErrorCodeDescription.NETWORK_SUCCESS, result);
        }
        else {
          return  new ResultData(ErrorCode.NETWORK_JSON_EXCEPTION, ErrorCodeDescription.NETWORK_JSON_EXCEPTION, response.data);
        }
      }
    } catch (e) {
      print(e.toString() + url);
      return ResultData(response.statusCode, ErrorCode.errorDescriptionWithCode(response.statusCode), response.data);
    }
  }
}

网络请求对象是集成的对象,自身可以访问的属性包含

  • baseURL
  • path
  • method
  • parameters
  • headers
  • result

在这个通道中,可以感知当前网络的所有对象的现状,对于post,get,put之类的网络请求,就可以在请求发起之前做好预先的判断,准备好对应的资源,发起请求。

同时,为了更好的使用网络请求回调的结果,规定了网络回调的结构,和对应错误编码的解析

///网络错误对应的描述
class ErrorCodeDescription {
  static const NETWORK_ERROR = "网络错误";

  static const NETWORK_TIMEOUT = "请求超时";

  static const NETWORK_JSON_EXCEPTION = "数据解析出错";

  static const NETWORK_NOT_REACHABLE = "没有网络";

  static const NETWORK_SUCCESS = "";

  static const NETWORK_UNDEFINED_ERROR = "未知错误";
}

///网络请求错误编码
class ErrorCode {
  ///网络错误
  static const NETWORK_ERROR = -1;

  ///网络超时
  static const NETWORK_TIMEOUT = -2;

  ///网络返回数据格式化一次
  static const NETWORK_JSON_EXCEPTION = -3;

  static const NETWORK_NOT_REACHABLE = -4;

  static const SUCCESS = 0;

  static errorDescriptionWithCode(int code) {
    switch (code) {
      case NETWORK_ERROR: return ErrorCodeDescription.NETWORK_ERROR;
      case NETWORK_TIMEOUT: return ErrorCodeDescription.NETWORK_TIMEOUT;
      case NETWORK_JSON_EXCEPTION: return ErrorCodeDescription.NETWORK_JSON_EXCEPTION;
      case NETWORK_NOT_REACHABLE: return ErrorCodeDescription.NETWORK_NOT_REACHABLE;
      case SUCCESS: return ErrorCodeDescription.NETWORK_ERROR;
      default: return ErrorCodeDescription.NETWORK_UNDEFINED_ERROR;
    }
  }
}

错误编码可以在上面的文件中追加,增加错误符号的可读性。对于结果结构的封装,使用的是如下对象的结构

/**
 * 网络结果数据
 */
class ResultData<T> {
  int status;
  var msg;
  T data;

  ResultData(this.status, this.msg, this.data);
}

使用结果时,可以先检测网络的status和msg,对于网络json回调结果采用了泛型的承接,DIO框架为我们提供了json转model的能力,在请求实例的时候,可以指定对应的结果类型,便于在业务逻辑中取值。这一步操作,可以在NetworlPipe中看到

网络框架到这里就已经做完了,除了请求通道本身,需要在不同的业务线做丰富。业务在使用的时候,只需在默认参数的基础上添加此实例对象的差异即可,例如

class RentListInstance extends NetworkPipe {
  @override
  // TODO: implement path
  String get path => "/home/business1";

  @override
  // TODO: implement method
  String get method => HttpMethod.GET;

  RentListInstance();
}

改用面向协议的网络结构之后,带来的好处

  • 细化到最后的网络实例对象更加聚合
  • 每个网络节点可读性更高
  • 每个网络节点逻辑聚合更高

下面是用一个逻辑图做的网络的总结